Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add D-Bus API for configuring NTP servers of systemd-timesyncd #207

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

sairon
Copy link
Member

@sairon sairon commented Sep 5, 2024

Add new time package that currently provides methods that configure NTP and FallbackNTP options of systemd-timesyncd service. The service is restarted when the values are changed. Example usage through gdbus:

gdbus call --system --dest io.hass.os
--object-path /io/hass/os/Time/Timesyncd
--method org.freedesktop.DBus.Properties.Set
io.hass.os.Time.Timesyncd NTPServer
"<['pool.ntp.org', 'time.google.com']>"

A lineinfile helper has been implemented to adjust Systemd unit files, the inspiration comes from Ansible's module of the same name, although the behavior is slightly different (hopefully still quite intuitive).

In the future, the time package could also handle system timezone and other time-related tasks.

Summary by CodeRabbit

  • New Features

    • Introduced time synchronization management through a D-Bus interface, enabling dynamic updates to NTP server configurations.
    • Added a utility for manipulating lines in text files, allowing for precise control over configuration entries.
  • Tests

    • Implemented unit tests for validating NTP configuration handling, ensuring robust functionality for line processing in configuration files.

@sairon sairon changed the title Add DBUS API for configuring NTP servers of systemd-timesyncd Add D-Bus API for configuring NTP servers of systemd-timesyncd Sep 5, 2024
Add new `time` package that currently provides methods that configure
NTP and FallbackNTP options of systemd-timesyncd service. The service is
restarted when the values are changed. Example usage through gdbus:

  gdbus call --system  --dest io.hass.os \
    --object-path /io/hass/os/Time/Timesyncd \
    --method org.freedesktop.DBus.Properties.Set \
    io.hass.os.Time.Timesyncd NTPServer \
    "<['pool.ntp.org', 'time.google.com']>"

A `lineinfile` helper has been implemented for adjusting Systemd unit
files, the inspiration comes from Ansible's module of the same name,
although the behavior is slightly different (hopefully still quite
intuitive). Unit tests for the core methods handling the file content
are included.

In the future, the `time` package could also handle system timezone and
other time-related tasks.
@sairon sairon force-pushed the timesyncd-ntp-config branch from df9f1d4 to 0f09f2b Compare September 5, 2024 14:02
@sairon
Copy link
Member Author

sairon commented Sep 5, 2024

@coderabbitai full review

Copy link

coderabbitai bot commented Sep 5, 2024

Walkthrough

Walkthrough

The changes introduce a new dependency on the os_time package for time management, enhance the main function to incorporate D-Bus time functionalities, and implement a new D-Bus interface for managing NTP server configurations in timesyncd.go. Additionally, a utility for line manipulation in files is added in lineinfile.go, along with comprehensive unit tests in lineinfile_test.go to validate the functionality of NTP configuration handling.

Changes

Files Change Summary
main.go, time/timesyncd.go, utils/lineinfile/lineinfile.go, utils/lineinfile/lineinfile_test.go Introduced new functionalities for time management and NTP server configuration through D-Bus. Added utility for line manipulation in files and implemented unit tests for validating NTP configuration handling.

Sequence Diagram(s)

sequenceDiagram
    participant Main
    participant DBus
    participant TimeSyncD
    participant LineInFile

    Main->>DBus: InitializeDBus(conn)
    DBus->>TimeSyncD: Setup NTP Configuration
    TimeSyncD->>LineInFile: Modify NTP Settings
    LineInFile-->>TimeSyncD: Return Updated Config
    TimeSyncD-->>DBus: Notify Configuration Update
    DBus-->>Main: Complete Initialization
Loading

Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 58f7f29 and ca6d552.

Files selected for processing (4)
  • main.go (2 hunks)
  • time/timesyncd.go (1 hunks)
  • utils/lineinfile/lineinfile.go (1 hunks)
  • utils/lineinfile/lineinfile_test.go (1 hunks)
Additional context used
Path-based instructions (1)
time/timesyncd.go (6)

Pattern */**(html|markdown|md): - For instructional content in documentation, use a direct and authoritative tone. Avoid expressions of politeness such as 'may' or 'please', and ensure the goal of the instruction is fronted.

  • Apply the Microsoft Style Guide to ensure documentation maintains clarity and conciseness.
  • In step-by-step instructions, front the location phrase in the instructional sentence.
  • In step-by-step instructions, front the 'goal' in the instructional sentence.
  • In step-by-step instructions, if in doubt what to front, front the 'goal' before the location phrase in the instructional sentence.
  • do not hyphenate terms like 'top-right' or 'bottom-left' with 'corner'

Pattern */**(html|markdown|md): - Use bold to mark UI strings.

  • If "" are used to mark UI strings, replace them by bold.

Pattern */**(html|markdown|md): - Be brief in your replies and don't add fluff like "thank you for..." and "Please let me know if"


Pattern */**(html|markdown|md): - Use sentence-style capitalization also in headings.


Pattern */**(html|markdown|md): do not comment on HTML used for icons


Pattern */**(html|markdown|md): Avoid flagging inline HTML for embedding videos in future reviews for this repository.

Additional comments not posted (16)
main.go (2)

4-4: Approved import statement.

The import of os_time from github.com/home-assistant/os-agent/time is essential for the new time management functionalities introduced in this PR.


74-74: Approved function call with a suggestion to verify initialization order.

The call to os_time.InitializeDBus(conn) is crucial for integrating the new time management functionalities. However, ensure that this initialization is correctly ordered relative to other D-Bus initializations and service readiness notifications.

Please verify that the initialization order in main() is optimal for the intended operations. This can be crucial for ensuring that the service is fully ready before it starts handling requests.

time/timesyncd.go (3)

3-13: Review of Imports and Constants

The imports are well-organized and relevant to the functionality being implemented. The constants objectPath, ifaceName, and timesyncdConf are clearly defined and appropriately named, which enhances readability and maintainability.


105-118: Function restartTimesyncd Review

The function correctly handles the interaction with systemd to restart the systemd-timesyncd service. The error handling is appropriate, ensuring that any issues with the D-Bus call are returned to the caller. This function is critical for applying changes and appears to be implemented correctly.


72-87: Function getTimesyncdConfigProperty Review

The function uses regular expressions to parse the configuration file, which is a robust choice for this task. However, the regular expression compilation is not cached, which can be inefficient if this function is called frequently. Consider compiling the regular expression once and reusing it.

Optimize regular expression usage by compiling it once:

-var propertyRegex = regexp.MustCompile(`^\s*(` + property + `=).*$`)

func getTimesyncdConfigProperty(property string) []string {
-	matches := regexp.MustCompile(property + `=([^\s#]+(?:\s+[^\s#]+)*)`).FindStringSubmatch(*value)
+	matches := propertyRegex.FindStringSubmatch(*value)
	...
}

Likely invalid or redundant comment.

utils/lineinfile/lineinfile_test.go (8)

34-46: Review: TestFindExisting Function

This function tests the scenario where the line containing "NTP=" is expected to be found after the "[Time]" section. The test is well-structured and checks both the presence of the result and its correctness.

  • Correctness: The test correctly splits the input string into lines and uses the processFind function to search for the pattern. The expected result is verified against the actual result.
  • Error Handling: The test includes error handling by checking if the result is nil and logs the result for debugging purposes.

Overall, the function is implemented correctly and covers the expected functionality.


48-54: Review: TestFindMissing Function

This function tests the scenario where the "NTP=" line is missing from the input content. The test is designed to expect a nil result, which is correctly asserted.

  • Correctness: The test uses the processFind function to attempt to find the "NTP=" line in a context where it does not exist, which should correctly return nil.
  • Error Handling: Proper error handling is implemented by checking if the result is not nil and then logging an error.

This test function is correctly implemented and effectively tests the expected behavior when the line is absent.


56-62: Review: TestFindMissingNotAfter Function

This function tests the scenario where the "NTP=" line is present but not after the "[Time]" section. The test correctly expects a nil result since the line is out of the specified context.

  • Correctness: The function correctly handles the context by using the processFind function with the specified pattern and section. The absence of the line in the expected section is correctly identified.
  • Error Handling: The test includes error handling by checking if the result is not nil and logs an error if it is.

The implementation of this test function is correct and it effectively tests the behavior when the line is not in the expected section.


64-75: Review: TestPresentSimple Function

This function tests the scenario where an existing "NTP=" line is modified to a new value. The test checks if the line is correctly updated.

  • Correctness: The test correctly identifies and replaces the existing "NTP=" line with a new value using the processPresent function. The expected result is compared against the actual result to ensure correctness.
  • Error Handling: The test handles potential errors by checking if the processed result matches the expected output and logs an error if it does not.

This test function is well-implemented and effectively tests the modification of an existing line.


77-93: Review: TestPresentNotAfter Function

This function tests the addition of a new "NTP=" line when it is not found after the "[Time]" section. The test ensures that the new line is added correctly in the specified section.

  • Correctness: The function correctly adds a new "NTP=" line when it is not found in the desired section. The result is compared against the expected output to ensure correctness.
  • Error Handling: The test includes error handling by checking if the processed result matches the expected output and logs an error if there are discrepancies.

The implementation of this test function is correct, and it effectively tests the addition of a new line in the specified section.


95-105: Review: TestPresentCommented Function

This function tests the scenario where a commented "NTP=" line is uncommented and set to a specified value. The test checks if the line is correctly uncommented and updated.

  • Correctness: The test correctly identifies and modifies the commented "NTP=" line using the processPresent function. The expected result is compared against the actual result to ensure correctness.
  • Error Handling: The test handles potential errors by checking if the processed result matches the expected output and logs an error if it does not.

This test function is well-implemented and effectively tests the modification of a commented line.


107-131: Review: TestPresentAppendedEOF Function

This function tests the scenario where a new "NTP=" line is added at the end of the file if it is not found. The test checks if the line is correctly appended.

  • Correctness: The test correctly appends a new "NTP=" line at the end of the file using the processPresent function. The expected result is compared against the actual result to ensure correctness.
  • Error Handling: The test includes error handling by checking if the processed result matches the expected output and logs an error if there are discrepancies.

This test function is well-implemented and effectively tests the appending of a new line at the end of the file.


133-161: Review: TestAbsent Function

This function tests the scenario where an existing "NTP=" line is removed from the file. The test checks if the line is correctly removed.

  • Correctness: The test correctly identifies and removes the "NTP=" line using the processAbsent function. The expected result is compared against the actual result to ensure correctness.
  • Error Handling: The test handles potential errors by checking if the processed result matches the expected output and logs an error if it does not.

This test function is well-implemented and effectively tests the removal of an existing line.

utils/lineinfile/lineinfile.go (3)

29-37: Review of Factory Functions: NewPresentParams and NewAbsentParams

These factory functions are a good design choice, simplifying the creation of Params objects with default values. The separation into two distinct functions for present and absent scenarios enhances readability and usability. No issues are found here.

Also applies to: 39-47


49-73: Review of Core Methods: Present and Absent

Both methods handle file presence checks and read operations robustly, with appropriate error handling. The use of os.Stat and os.ReadFile is correct and idiomatic in Go. The error logging within these methods is consistent and will aid in debugging. However, the actual line processing logic in processPresent and processAbsent might be complex and warrants a separate review to ensure correctness and efficiency.

Also applies to: 75-101


238-253: Review of File Writing Method: writeFile

The writeFile method uses the atomic package to ensure that file writes are safe and atomic, which is crucial for file manipulation utilities. The method correctly checks for the newline at the end of the file before writing, which is a good detail to handle. The error logging is consistent with other parts of the module.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Comment on lines +21 to +25
var (
optNTPServer []string
optFallbackNTPServer []string
configFile = lineinfile.LineInFile{FilePath: timesyncdConf}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global Variables and Configuration File Initialization

The use of global variables (optNTPServer, optFallbackNTPServer) is generally discouraged as it can lead to issues with state management, especially in a multi-threaded environment. Consider encapsulating these within the timesyncd struct to better manage their scope and lifecycle.

Suggest encapsulating global variables within the timesyncd struct:

-type timesyncd struct {
-	conn  *dbus.Conn
-	props *prop.Properties
+type timesyncd struct {
+	conn                *dbus.Conn
+	props               *prop.Properties
+	optNTPServer        []string
+	optFallbackNTPServer []string
}

Committable suggestion was skipped due to low confidence.

time/timesyncd.go Show resolved Hide resolved
Comment on lines +40 to +54
func setNTPServer(c *prop.Change) *dbus.Error {
servers, ok := c.Value.([]string)
if !ok {
return dbus.MakeFailedError(fmt.Errorf("invalid type for NTPServer"))
}

value := strings.Join(servers, " ")

if err := setTimesyncdConfigProperty("NTP", value); err != nil {
return dbus.MakeFailedError(err)
}

optNTPServer = servers
return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function setNTPServer Review

This function handles the setting of NTP servers via D-Bus properties. The error handling and type assertion are correctly implemented. However, the function directly modifies a global variable, which could lead to race conditions or other concurrency issues. Consider using a mutex or similar synchronization mechanism if the API is expected to handle concurrent modifications.

Suggest adding synchronization to handle concurrent access:

+import "sync"

+var mutex sync.Mutex

func setNTPServer(c *prop.Change) *dbus.Error {
+	mutex.Lock()
+	defer mutex.Unlock()
	...
}

Committable suggestion was skipped due to low confidence.

utils/lineinfile/lineinfile.go Show resolved Hide resolved
Comment on lines +103 to +150
func processPresent(inLines []string, params Params) ([]string, error) {
var outLines []string

if params.Before != "" && params.After != "EOF" {
err := fmt.Errorf("cannot specify both Before and After")
return nil, err
}

needsBefore := params.Before != ""
needsAfter := params.After != "EOF"
afterRegexp, _ := re.Compile(params.After)
beforeRegexp, _ := re.Compile(params.Before)

var beforeIndex = -1
var afterIndex = -1
var foundIndex = -1

for idx, curr := range inLines {
outLines = append(outLines, curr)
if needsBefore && beforeIndex < 0 && beforeRegexp.MatchString(curr) {
beforeIndex = idx
continue
}
if needsAfter && afterIndex < 0 && afterRegexp.MatchString(curr) {
afterIndex = idx
continue
}
if (needsAfter && afterIndex >= 0 || needsBefore && beforeIndex >= 0) && foundIndex < 0 && params.Regexp.MatchString(curr) {
foundIndex = idx
}
}

if foundIndex >= 0 {
// replace found line with the params.Line
outLines[foundIndex] = params.Line
} else if params.After == "EOF" {
outLines = append(outLines, params.Line)
} else if params.Before == "BOF" {
outLines = append([]string{params.Line}, outLines...)
} else if params.After != "" {
if afterIndex >= 0 {
// insert after the line matching the After regexp
outLines = append(outLines[:afterIndex+1], append([]string{params.Line}, outLines[afterIndex+1:]...)...)
}
}

return outLines, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of Line Processing Functions: processPresent and processAbsent

These functions contain complex logic for inserting or removing lines based on the provided parameters. The error handling for conflicting Before and After parameters is correctly implemented. However, the regex compilation inside the loop (lines 113, 114, 162, 163) should be moved outside the loop to optimize performance, as compiling the same regex repeatedly is inefficient.

Consider refactoring to compile the regex expressions once before the loop starts to enhance performance.

- afterRegexp, _ := re.Compile(params.After)
- beforeRegexp, _ := re.Compile(params.Before)
+ var afterRegexp, beforeRegexp *re.Regexp
+ var err error
+ if params.After != "" {
+     afterRegexp, err = re.Compile(params.After)
+     if err != nil {
+         return nil, err
+     }
+ }
+ if params.Before != "" {
+     beforeRegexp, err = re.Compile(params.Before)
+     if err != nil {
+         return nil, err
+     }
+ }

Also applies to: 152-190

Comment on lines +192 to +236
func (l LineInFile) Find(regexp string, after string, allowMissing bool) (*string, error) {
if _, err := os.Stat(l.FilePath); os.IsNotExist(err) {
if allowMissing {
return nil, nil
}
logging.Error.Printf("File %s does not exist: %s", l.FilePath, err)
return nil, err
}

content, err := os.ReadFile(l.FilePath)
if err != nil {
fmt.Print(err)
return nil, err
}

lines := strings.Split(string(content), "\n")

return processFind(regexp, after, lines), nil
}

func processFind(regexp string, after string, inLines []string) *string {
if inLines == nil {
return nil
}

lineRegexp, _ := re.Compile(regexp)
afterRegexp, _ := re.Compile(after)

var foundAfter = false

for _, curr := range inLines {
if !foundAfter && afterRegexp.MatchString(curr) {
foundAfter = true
continue
}
if after != "" && !foundAfter {
continue
}
if lineRegexp.MatchString(curr) {
return &curr
}
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of Auxiliary Method: Find

The Find method provides functionality to search for a line based on a regex, optionally after a certain line. The implementation handles file existence checks and reads the file content correctly. The separation of the regex matching logic into processFind is a good practice, keeping the Find method clean and focused. However, the error handling could be improved by logging errors consistently as done in other methods.

Enhance error handling by adding logging for all errors in the Find method to maintain consistency with other methods in the file.

- fmt.Print(err)
+ logging.Error.Printf("Error reading file %s: %s", l.FilePath, err)
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (l LineInFile) Find(regexp string, after string, allowMissing bool) (*string, error) {
if _, err := os.Stat(l.FilePath); os.IsNotExist(err) {
if allowMissing {
return nil, nil
}
logging.Error.Printf("File %s does not exist: %s", l.FilePath, err)
return nil, err
}
content, err := os.ReadFile(l.FilePath)
if err != nil {
fmt.Print(err)
return nil, err
}
lines := strings.Split(string(content), "\n")
return processFind(regexp, after, lines), nil
}
func processFind(regexp string, after string, inLines []string) *string {
if inLines == nil {
return nil
}
lineRegexp, _ := re.Compile(regexp)
afterRegexp, _ := re.Compile(after)
var foundAfter = false
for _, curr := range inLines {
if !foundAfter && afterRegexp.MatchString(curr) {
foundAfter = true
continue
}
if after != "" && !foundAfter {
continue
}
if lineRegexp.MatchString(curr) {
return &curr
}
}
return nil
}
func (l LineInFile) Find(regexp string, after string, allowMissing bool) (*string, error) {
if _, err := os.Stat(l.FilePath); os.IsNotExist(err) {
if allowMissing {
return nil, nil
}
logging.Error.Printf("File %s does not exist: %s", l.FilePath, err)
return nil, err
}
content, err := os.ReadFile(l.FilePath)
if err != nil {
logging.Error.Printf("Error reading file %s: %s", l.FilePath, err)
return nil, err
}
lines := strings.Split(string(content), "\n")
return processFind(regexp, after, lines), nil
}
func processFind(regexp string, after string, inLines []string) *string {
if inLines == nil {
return nil
}
lineRegexp, _ := re.Compile(regexp)
afterRegexp, _ := re.Compile(after)
var foundAfter = false
for _, curr := range inLines {
if !foundAfter && afterRegexp.MatchString(curr) {
foundAfter = true
continue
}
if after != "" && !foundAfter {
continue
}
if lineRegexp.MatchString(curr) {
return &curr
}
}
return nil
}

@@ -0,0 +1,175 @@
package time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid clash with the go time package, I'd go for something else, like timesync.

Suggested change
package time
package timesync

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to have the same prefix for the DBus interfaces controlling other time-related features in the future, and so far we've got it synchronized with package naming. In the end the only clash is in the main file, where it can be simply aliased.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this is mostly about filling the gap of missing D-Bus interfaces in systemd. Ideally, we would extend systemd, but this is much more time consuming, so I am fine doing our thing now here.

But, with that in mind, I think it make sense to align with the systemd D-Bus naming: Systemd has two time/date related D-Bus services: org.freedesktop.timesync1 and org.freedesktop.timedate1. What we currently miss is setting the system NTP server, which most likely should be part of the org.freedesktop.timesync1 service.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than the Systemd D-Bus naming, consider naming of our D-Bus interfaces - now it's io.hass.os.Time.Timesyncd, which is implemented in time/timesyncd.go. If we added e.g. interface for timezone setting (leave aside the fact there's some method in org.freedesktop.timedate1 that might be used instead), a logical path would be io.hass.os.Time.Timezone and the implementation would go to time/timezone.go. If the package should be renamed, then we should also adjust D-Bus paths and interfaces.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the other discussion in mind, it kinda seems what we need to build here is mostly a systemd config file manager. So aligning the naming to the config file names probably make sense 🤔

@@ -0,0 +1,253 @@
package lineinfile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, did you consider using something existing? There is for instance this library: https://github.com/sergeymakinen/go-systemdconf. Not sure if works for the daemon config files, the systemd.syntax man page says it has the same syntax so might work actually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did some research there, most of them seemed too bloated, complex and/or unmaintained, so I didn't want to get trapped in the future by introducing such dependency (e.g. this one depends on 5 years old release of a CSS processing library). In the end I went with the "lineinfile" concept, which is powerful enough it's able to drive configuration changes by many Ansible roles in the real world, and it's easy to understand and troubleshoot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, yeah unmaintained lib is worse, I agree.

conn: conn,
}

optNTPServer = getNTPServers()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this could be a bit racy, depending on when OS Agent/NetworkManager hook gets called 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation only touches the persistent configuration file, so NetworkManager hook doesn't affect it in any way. If we want to read the dynamically obtained NTPs written by the NM hook, maybe using a different method/property would be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we even need these getters? There is FallbackNTPServers and SystemNTPServers available on the /org/freedesktop/timesync1 object of the org.freedesktop.timesync1 service.

In a way, this interface to me is mainly a stopgap until systemd-timesyncd has a similar functionality.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those can be amalgamation of multiple configuration files as described in man timesyncd.conf. This interface exposes what is statically configured in the file we'd like to be the source of truth for the configuration. I can imagine the "native" Systemd attributes could be used for sanity checking, raising an issue if the configured value doesn't match what's in the config (which e.g. means user has created extra configuration files). But it would be confusing if you had only the setters, and under some circumstances the getters would return different value than what you have set.

I also doubt that Systemd will ever get similar functionality. There are the runtime NTP servers but those we can't use because we need to apply the configuration before our "configuration daemon" (Supervisor) is started. AFAIK Systemd doesn't have a way to edit configuration files through D-Bus API anywhere, and some things can be only configured only through config files.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those can be amalgamation of multiple configuration files as described in man timesyncd.conf. This interface exposes what is statically configured in the file we'd like to be the source of truth for the configuration. I can imagine the "native" Systemd attributes could be used for sanity checking, raising an issue if the configured value doesn't match what's in the config (which e.g. means user has created extra configuration files). But it would be confusing if you had only the setters, and under some circumstances the getters would return different value than what you have set.

Right, I definitely would not forward the getter here to D-Bus, that would indeed be confusing.

I am just wonder, if we cannot get away with a more minimal D-Bus interface. Sure, we already have it implemented (with this PR), but there is always maintenance cost.

I also doubt that Systemd will ever get similar functionality. There are the runtime NTP servers but those we can't use because we need to apply the configuration before our "configuration daemon" (Supervisor) is started. AFAIK Systemd doesn't have a way to edit configuration files through D-Bus API anywhere, and some things can be only configured only through config files.

Yeah you probably be right on that there won't be a timesync1 setter. But there is already a systemd-networkd API to set NTP servers:
https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.network1.html#The%20Manager%20Object

I think systemd sees NTP server configuration a network manager thing, just like DNS. And it should be handled and stored by the network manager, and transferred at the appropriate time (the appropriate link is actually ready). I mean, you can imagine a system where you have two network links, each have their own local NTP servers.. You can't simply mix and match in that scenario. Ofc, this won't be the typical HAOS scenario, but it helps to understand how things develop. On the other hand, the system/fallback NTP's will always be global.

Researching a bit more, I came accross this Cockpit issue: cockpit-project/cockpit#7987 (comment)

And the linked systemd issue: systemd/systemd#7593 (comment) and related systemd/systemd#27469. The gist is that systemd-networkd (and I guess with that systemd-timesyncd) lacks a configuration management API. It is all meant to go through files, and that is what it is currently.

Maybe a generic systemd configuration file management API would be better? 🤔 On the other hand, maybe it is better to have OS Agent as a bit of a firewall for this 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think systemd sees NTP server configuration a network manager thing, just like DNS. And it should be handled and stored by the network manager, and transferred at the appropriate time (the appropriate link is actually ready). I mean, you can imagine a system where you have two network links, each have their own local NTP servers.

Yes, but I think this is not achievable without using systemd-networkd at the same time. Currently we store the network config in NetworkManager's config files, where we lack any mechanism to save per-link user-configured NTP servers, so we'd have to introduce one (i.e. have our own config files/attributes of individual NM connections that'd somehow propagate to timesyncd config). But the complexity of such implementation would heavily outweigh any maintenance cost of the current approach to this problem.

Anyway, if we shall consider multihomed setups which even require different NTP servers on individual links, then I think this discussion turns academic. AFAIK most of the multihomed setups are not what HA considers anywhere, leave aside the fact that even just the NTP config alone seems too advanced for some 😅 We really should think about the real use case, and the way we want to handle NTP configuration on user's side.

I agree though that we can take a different approach on this problem and instead of exposing "time subsystem" APIs, we could have "system configuration" instead, if that makes more sense to you.

Copy link

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.

@github-actions github-actions bot added the stale label Oct 12, 2024
@github-actions github-actions bot closed this Oct 19, 2024
@agners agners added no-stale and removed stale labels Oct 21, 2024
@agners agners reopened this Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants